/*
 *  Copyright (c) 2019-present, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree.
 */

#include <folly/portability/GMock.h>
#include <folly/portability/GTest.h>

#include <fizz/crypto/Utils.h>
#include <fizz/crypto/test/TestUtil.h>
#include <fizz/extensions/delegatedcred/PeerDelegatedCredential.h>

using namespace folly;

using namespace testing;
using namespace fizz::test;

namespace fizz {
namespace extensions {
namespace test {

// @lint-ignore-every PRIVATEKEY

/*
 *  Randomly generated ECDSA-P256 private key
 *  Command: openssl ecparam -name secp256r1 -genkey
 *  Output: Randomly generated ECDSA-P256 private key
 */
StringPiece kP256CredCertKey = R"(
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILzXE+AGe6Bg3yQ45nrENlmsJJbOhYBI3nJim5V18MQjoAoGCCqGSM49
AwEHoUQDQgAEBvXlRUvxOcYCjrRSOAoQ28kFYS7X1R6a/YozsLjlSv+g1sZVPunK
LCKJ7GnAlE2Ezs5aGDutdPJV5Uj8e1FpCw==
-----END EC PRIVATE KEY-----
)";

// clang-format off
/*
 *  Delegated credential certificate generated by kP256CredCertKey
 *  Prerequisites:
 *    - P133567922 in config.cfg
 *    - kP256CredCertKey in p256_key.pem
 *  Command: openssl req -new -key p256_key.pem -x509 -nodes -days 365 -config config.cfg
 *  Output: Self-signed delegation certificate
 */
// clang-format on
StringPiece kP256CredCert = R"(
-----BEGIN CERTIFICATE-----
MIICBTCCAaygAwIBAgIJAOsXblJ0XNeJMAoGCCqGSM49BAMCMFExCzAJBgNVBAYT
AlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29t
cGFueSBMdGQxDTALBgNVBAMMBEZpenowHhcNMjAwNjE3MjMyNzEzWhcNMjEwNjE3
MjMyNzEzWjBRMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRww
GgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMQ0wCwYDVQQDDARGaXp6MFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEBvXlRUvxOcYCjrRSOAoQ28kFYS7X1R6a/Yoz
sLjlSv+g1sZVPunKLCKJ7GnAlE2Ezs5aGDutdPJV5Uj8e1FpC6NtMGswHQYDVR0O
BBYEFLJ7AdynN3OOrZnG4jcCimwssWZyMB8GA1UdIwQYMBaAFLJ7AdynN3OOrZnG
4jcCimwssWZyMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgHmMA4GCSsGAQQBgtpL
LAQBADAKBggqhkjOPQQDAgNHADBEAiAOHfxiKeMC4ZndoOiqjsip2S803ePrxRqu
TrE2DBm4lwIgFEzqvc8SduigoB8k6i2reloeQcC03yX9AbM6ucPiIiA=
-----END CERTIFICATE-----
)";

/*
 *  Randomly generated ECDSA-P256 private key
 *  Command: openssl ecparam -name secp256r1 -genkey
 *  Output: Randomly generated ECDSA-P256 private key
 */
StringPiece kP256DelegatedCredKey = R"(
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIOGZVfvLK8gObg4sM/K98plCrApsNvZMg0FPW/zJZf/yoAoGCCqGSM49
AwEHoUQDQgAEbApTBiqLqh8gwgWKfiePUwy56THkhCla2elokwvJxbJ6VZFjVxJ9
n29CHucXPw+V0ob2w9DBjgBas6kwJ9hLeQ==
-----END EC PRIVATE KEY-----
)";

// clang-format off
/*
 *  Delegated credential generated using kP256CredCert, kP256CredCertKey & kP256DelegatedCredKey
 *  Prerequisites:
 *    - kP256CredCert in cert.pem
 *    - kP256CredCertKey in p256_key.pem
 *    - kP256DelegatedCredKey in p256_dc_key.pem
 *  Command: buck run //fizz/tool:fizz -- gendc -cert cert.pem -key p256_key.pem -credkey p256_dc_key.pem | xxd -p
 *  Output: Hex-encoded delegated credential
 */
// clang-format on
StringPiece kP256DelegatedCred{
    "000155ce040300005b3059301306072a8648ce3d020106082a8648ce3d03"
    "0107034200046c0a53062a8baa1f20c2058a7e278f530cb9e931e484295a"
    "d9e968930bc9c5b27a55916357127d9f6f421ee7173f0f95d286f6c3d0c1"
    "8e005ab3a93027d84b79040300473045022021b29022bfdfac5e4c21a9c9"
    "a50eedb086be632747f1e1dccddd24fa52606953022100a1b9d58e2c3064"
    "e1ad2b7929c80eb9a99fef69343e8ade527c8a984c2e01bbb8"};

// Buffer used for signature/verification
StringPiece kVerifyBuffer{
    "8bc8098c4f45d1c9ea354955f5c99a50b442c6ab8b58c6623582b60ddc8c2ef2"};

// Signature of kVerifyBuffer using kP256DelegatedCred
StringPiece kP256Signature{
    "304402203c8a625e97a07ec562f326dfd95e3e891ea049d24e0bfe44dbb8"
    "a099a58d83cc0220101afdd3f0e51c771d0ea872293e006d0546d8821b0f"
    "a45d6c64a14fcf918ffb"};

class PeerDelegatedCredentialTest : public Test {
 public:
  void SetUp() override {
    CryptoUtils::init();
  }

  Buf toBuf(const StringPiece& hex) {
    auto data = unhexlify(hex);
    return IOBuf::copyBuffer(data.data(), data.size());
  }

  DelegatedCredential getCredential() {
    Extension ext;
    ext.extension_type = ExtensionType::delegated_credential;
    ext.extension_data = toBuf(kP256DelegatedCred);
    std::vector<Extension> exts;
    exts.push_back(std::move(ext));
    return *getExtension<DelegatedCredential>(exts);
  }

  void expectThrows(std::function<void()> f, std::string errorStr) {
    std::string what;
    try {
      f();
    } catch (const FizzException& e) {
      what = e.what();
    }

    EXPECT_THAT(what, HasSubstr(errorStr));
  }
};

TEST_F(PeerDelegatedCredentialTest, TestCredentialVerify) {
  auto cred = getCredential();
  auto cert = getCert(kP256CredCert);
  auto pubKeyRange = cred.public_key->coalesce();
  auto addr = pubKeyRange.data();
  folly::ssl::EvpPkeyUniquePtr pubKey(
      d2i_PUBKEY(nullptr, &addr, pubKeyRange.size()));
  auto peerCred = std::make_shared<PeerDelegatedCredential<KeyType::P256>>(
      std::move(cert), std::move(pubKey), std::move(cred));

  peerCred->verify(
      SignatureScheme::ecdsa_secp256r1_sha256,
      CertificateVerifyContext::Server,
      toBuf(kVerifyBuffer)->coalesce(),
      toBuf(kP256Signature)->coalesce());
}

TEST_F(PeerDelegatedCredentialTest, TestCredentialVerifyWrongCert) {
  auto cred = getCredential();
  auto cert = getCert(kP256Certificate);
  auto pubKeyRange = cred.public_key->coalesce();
  auto addr = pubKeyRange.data();
  folly::ssl::EvpPkeyUniquePtr pubKey(
      d2i_PUBKEY(nullptr, &addr, pubKeyRange.size()));
  auto peerCred = std::make_shared<PeerDelegatedCredential<KeyType::P256>>(
      std::move(cert), std::move(pubKey), std::move(cred));

  expectThrows(
      [&]() {
        peerCred->verify(
            SignatureScheme::ecdsa_secp256r1_sha256,
            CertificateVerifyContext::Server,
            toBuf(kVerifyBuffer)->coalesce(),
            toBuf(kP256Signature)->coalesce());
      },
      "failed to verify signature on credential");
}

TEST_F(PeerDelegatedCredentialTest, TestCredentialVerifyWrongAlgo) {
  auto cred = getCredential();
  auto cert = getCert(kP256CredCert);
  auto pubKeyRange = cred.public_key->coalesce();
  auto addr = pubKeyRange.data();
  folly::ssl::EvpPkeyUniquePtr pubKey(
      d2i_PUBKEY(nullptr, &addr, pubKeyRange.size()));
  auto peerCred = std::make_shared<PeerDelegatedCredential<KeyType::P256>>(
      std::move(cert), std::move(pubKey), std::move(cred));

  // Should fail early due to mismatch with credential
  expectThrows(
      [&]() {
        peerCred->verify(
            SignatureScheme::ecdsa_secp521r1_sha512,
            CertificateVerifyContext::Server,
            toBuf(kVerifyBuffer)->coalesce(),
            toBuf(kP256Signature)->coalesce());
      },
      "certificate verify didn't use credential's algorithm");
}

TEST_F(PeerDelegatedCredentialTest, TestCredentialVerifyBitFlip) {
  auto cred = getCredential();
  auto cert = getCert(kP256CredCert);
  auto pubKeyRange = cred.public_key->coalesce();
  auto addr = pubKeyRange.data();
  folly::ssl::EvpPkeyUniquePtr pubKey(
      d2i_PUBKEY(nullptr, &addr, pubKeyRange.size()));
  auto peerCred = std::make_shared<PeerDelegatedCredential<KeyType::P256>>(
      std::move(cert), std::move(pubKey), std::move(cred));

  auto sig = toBuf(kP256Signature);
  sig->writableData()[1] ^= 0x20;
  EXPECT_THROW(
      peerCred->verify(
          SignatureScheme::ecdsa_secp256r1_sha256,
          CertificateVerifyContext::Server,
          toBuf(kVerifyBuffer)->coalesce(),
          sig->coalesce()),
      std::runtime_error);
}

TEST_F(PeerDelegatedCredentialTest, TestCredentialVerifySizeMismatch) {
  auto cred = getCredential();
  auto cert = getCert(kP256CredCert);
  auto pubKeyRange = cred.public_key->coalesce();
  auto addr = pubKeyRange.data();
  folly::ssl::EvpPkeyUniquePtr pubKey(
      d2i_PUBKEY(nullptr, &addr, pubKeyRange.size()));
  auto peerCred = std::make_shared<PeerDelegatedCredential<KeyType::P256>>(
      std::move(cert), std::move(pubKey), std::move(cred));

  auto sig = toBuf(kP256Signature);
  sig->prependChain(IOBuf::copyBuffer("0"));
  EXPECT_THROW(
      peerCred->verify(
          SignatureScheme::ecdsa_secp256r1_sha256,
          CertificateVerifyContext::Server,
          toBuf(kVerifyBuffer)->coalesce(),
          sig->coalesce()),
      std::runtime_error);
}

} // namespace test
} // namespace extensions
} // namespace fizz
